package net.flydy.ywxcx.wxcloudrun.common;

import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Setter
@Getter
public class MyHttpClient {

    protected static final Logger logger = LoggerFactory.getLogger(MyHttpClient.class);
    private String logFlag = "MyHttpClient";

    private int connectTimeout;
    private int socketTimeout;
    private int connectionRequestTimeout;
    private int maxConnPerRoute;
    private int maxConnTotal;
    private int retryTimes;
    private long keepAliveTime;
    /**
     * 最大闲置时间. ms
     */
    private int validateAfterInactivity;

    /**
     * 监控线程间隔run时间
     */
    private long clearCheckInterval;

    private int closeIdletime;

    /**
     * 逗号分隔，冒号分隔message
     * org.apache.http.NoHttpResponseException,java.io.InterruptedIOException,java.net.SocketException:Connection reset
     */
    private String tryExceptions;

    /**
     * SSLHandshakeException,UnknownHostException,ConnectTimeoutException,SSLException
     * 逗号分隔，冒号分隔message
     */
    private String ignorTryExceptions;

    /**
     * 幂等是否要重试
     */
    private String tryRequestMd = "1";

    /**
     * 请求未发送完全是否要重试
     */
    private String tryRequestNotSent = "1";

    private CloseableHttpClient client;
    private RequestConfig requestConfig;
    private PoolingHttpClientConnectionManager poolingConnectionManager;

    private class IdleConnectionMonitorThread extends Thread {
        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(
                PoolingHttpClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(clearCheckInterval);
                        connMgr.closeExpiredConnections();
                        connMgr.closeIdleConnections(closeIdletime, TimeUnit.MILLISECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                shutdown();
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }

    private class RetryClass {
        Class clazz;
        String message;

        public RetryClass(String clazzStr, String message) throws Exception {
            clazz = Thread.currentThread().getContextClassLoader().loadClass(clazzStr);
            this.message = message;
        }

        boolean isMatch(IOException exception) {
            boolean ret = clazz.isInstance(exception);
            if (ret && StringUtils.isNotBlank(message)) {
                ret = StringUtils.contains(exception.getMessage(), message);
            }
            return ret;
        }
    }

    private class MyHttpRequestRetryHandler implements HttpRequestRetryHandler {
        int tryTimes = 2;
        /**
         * NoHttpResponseException、java.io.InterruptedIOException、SocketException:Connection reset
         */
        List<RetryClass> tryExceptions;
        /**
         * SSLHandshakeException、UnknownHostException、ConnectTimeoutException、SSLException
         */
        List<RetryClass> ignorTryExceptions;

        public MyHttpRequestRetryHandler(int tryTimes, List<String> tryStrs, List<String> ignorStrs) throws Exception {
            this.tryTimes = tryTimes;
            tryExceptions = new ArrayList<>();
            for (String item : tryStrs) {
                String[] vs = item.split(":");
                if (vs.length >= 2) {
                    tryExceptions.add(new RetryClass(vs[0], vs[1]));
                } else {
                    tryExceptions.add(new RetryClass(vs[0], null));
                }
            }

            ignorTryExceptions = new ArrayList<>();
            for (String item : ignorStrs) {
                String[] vs = item.split(":");
                if (vs.length >= 2) {
                    ignorTryExceptions.add(new RetryClass(vs[0], vs[1]));
                } else {
                    ignorTryExceptions.add(new RetryClass(vs[0], null));
                }

            }
        }

        @Override
        public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
            if (executionCount >= tryTimes) {
                if (logger.isWarnEnabled()) {
                    logger.warn(logFlag + "{}", String.format("跳过重试(重试过多) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
                }
                return false;
            }

            for (RetryClass retryClass : ignorTryExceptions) {
                if (retryClass.isMatch(exception)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn(logFlag + "{}", String.format("跳过重试(ignorTry) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
                    }
                    return false;
                }
            }

            for (RetryClass retryClass : tryExceptions) {
                if (retryClass.isMatch(exception)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn(logFlag + "{}", String.format("将重试(inTry) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
                    }
                    return true;
                }
            }
            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest request = clientContext.getRequest();
            // 如果请求是幂等的，就再次尝试
            if ("1".equals(tryRequestMd) && !(request instanceof HttpEntityEnclosingRequest)) {
                if (logger.isWarnEnabled()) {
                    logger.warn(logFlag + "{}", String.format("将重试(请求幂等) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
                }
                return true;
            }
            if ("1".equals(tryRequestNotSent) && !clientContext.isRequestSent()) {
                // Retry if the request has not been sent fully or
                // if it's OK to retry methods that have been sent
                if (logger.isWarnEnabled()) {
                    logger.warn(logFlag + "{}", String.format("将重试(请求未全发过去) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
                }
                return true;
            }
            if (logger.isWarnEnabled()) {
                logger.warn(logFlag + "{}", String.format("跳过重试(end) executionCount=%s, excep=%s, excepMessage=%s", executionCount, exception.getClass(), exception.getMessage()));
            }
            return false;
        }
    }

    public MyHttpClient(){

    }
    public MyHttpClient(int connectTimeout, int socketTimeout, int connectionRequestTimeout, int maxConnPerRoute, int maxConnTotal, int retryTimes, int validateAfterInactivity, long clearCheckInterval, long keepAliveTime, String tryExceptions, String ignorTryExceptions, int closeIdletime, String tryRequestMd, String tryRequestNotSent) throws Exception {
        this.connectTimeout = connectTimeout;
        this.socketTimeout = socketTimeout;
        this.connectionRequestTimeout = connectionRequestTimeout;
        this.maxConnPerRoute = maxConnPerRoute;
        this.maxConnTotal = maxConnTotal;
        this.retryTimes = retryTimes;
        this.validateAfterInactivity = validateAfterInactivity;
        this.clearCheckInterval = clearCheckInterval;
        this.keepAliveTime = keepAliveTime;
        this.tryExceptions = tryExceptions;
        this.ignorTryExceptions = ignorTryExceptions;
        this.closeIdletime = closeIdletime;
        this.tryRequestMd = tryRequestMd;
        this.tryRequestNotSent = tryRequestNotSent;
        init();
    }

    public void init() throws Exception{
        requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectTimeout).setSocketTimeout(socketTimeout).setConnectionRequestTimeout(connectionRequestTimeout)
                .setCircularRedirectsAllowed(true)
                .setMaxRedirects(retryTimes + 1)
                .build()
        ;

        //设置keep alive 策略
        ConnectionKeepAliveStrategy myStrategy = new DefaultConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                long keepAlive = super.getKeepAliveDuration(response, context);
                if (keepAlive == -1) {
                    keepAlive = keepAliveTime;
                }
                return keepAlive;
            }
        };
        // 信任所有
        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null,
                (TrustStrategy) (chain, authType) -> true).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
                NoopHostnameVerifier.INSTANCE);

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                // 正常的SSL连接会验证码所有证书信息
                // .register("https", new SSLConnectionSocketFactory(sslcontext)).build();
                //  只忽略域名验证码
                .register("https", sslsf).build();

        this.poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        poolingConnectionManager.setMaxTotal(maxConnTotal);
        poolingConnectionManager.setDefaultMaxPerRoute(maxConnPerRoute);
        poolingConnectionManager.setValidateAfterInactivity(this.validateAfterInactivity);

        HttpRequestRetryHandler retryHandler = new MyHttpRequestRetryHandler(this.retryTimes, Arrays.asList(tryExceptions.split(",")), Arrays.asList(ignorTryExceptions.split(",")));

        SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(socketTimeout).build();
        poolingConnectionManager.setDefaultSocketConfig(socketConfig);
        client = HttpClients.custom().setSSLSocketFactory(sslsf)
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(poolingConnectionManager)
                .setKeepAliveStrategy(myStrategy)
                .setConnectionManagerShared(false)
                .setRetryHandler(retryHandler)
                .setRedirectStrategy(new DefaultRedirectStrategy(new String[]{HttpGet.METHOD_NAME, HttpHead.METHOD_NAME, HttpPost.METHOD_NAME}))
                .disableConnectionState().build();

        IdleConnectionMonitorThread idleConnectionMonitorThread = new IdleConnectionMonitorThread(poolingConnectionManager);
        idleConnectionMonitorThread.start();
        idleConnectionMonitorThread.join(2000);
    }
    public void destroy() throws Exception{
        poolingConnectionManager.shutdown();
        client.close();
    }
    public String[] getJsonHeader() {
        return new String[]{"Content-Type", "application/json;charset=UTF-8"};
    }

    public String get(String url, HashMap<String, String> headers) throws Exception {
        String ret;
        if (logger.isInfoEnabled()) {
            String sb = "url=" + url + "\n" +
                    "headers = " + JSONUtil.toJsonStr(headers) + "\n" ;
            logger.info(logFlag + "{}", sb);
        }
        HttpGet httpMethod = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            httpMethod.setConfig(requestConfig);
            if (headers != null) {
                headers.forEach((key, value) -> httpMethod.setHeader(key, value));
            }
            response = client.execute(httpMethod);
            int status = response.getStatusLine().getStatusCode();
            if (status >= 200 && status < 300) {
                HttpEntity responseEntity = response.getEntity();
                ret = responseEntity != null ? EntityUtils.toString(responseEntity, "UTF-8") : null;
            } else {
                throw new ClientProtocolException("Unexpected response status: " + status);
            }
            if (logger.isInfoEnabled() && response != null) {
                Header[] respHeaders = response.getAllHeaders();
                StringBuilder sb = new StringBuilder();
                sb.append("响应\n");
                if (respHeaders != null) {
                    sb.append("headers = \n");
                    for (Header header : respHeaders) {
                        sb.append(header.getName() + "=" + header.getValue() + "\n");
                    }
                }
                sb.append("responseData=" + ret);
                logger.info(logFlag + "{}", sb);
            }

        } finally {
            releaseRequest(httpMethod, response);
        }
        return ret;
    }

    public String postJson(String url, String postData, HashMap<String, String> headers) throws Exception {
        String ret;
        if (logger.isInfoEnabled()) {
            String sb = "url=" + url + "\n" +
                    "headers = " + JSONUtil.toJsonStr(headers) + "\n" +
                    "postData=" + postData;
            logger.info(logFlag + "{}", sb);
        }
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpResponse response = null;
        try {
            StringEntity entity = new StringEntity(postData, "UTF-8");
            if (headers != null) {
                headers.forEach((key, value) -> httpPost.setHeader(key, value));
            }
            httpPost.setConfig(requestConfig);
            httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
            httpPost.setEntity(entity);

            response = client.execute(httpPost);
            int status = response.getStatusLine().getStatusCode();
            if (status >= 200 && status < 300) {
                HttpEntity responseEntity = response.getEntity();
                ret = responseEntity != null ? EntityUtils.toString(responseEntity, "UTF-8") : null;
            } else {
                throw new ClientProtocolException("Unexpected response status: " + status);
            }
            if (logger.isInfoEnabled() && response != null) {
                Header[] respHeaders = response.getAllHeaders();
                StringBuilder sb = new StringBuilder();
                sb.append("响应\n");
                if (respHeaders != null) {
                    sb.append("headers = \n");
                    for (Header header : respHeaders) {
                        sb.append(header.getName() + "=" + header.getValue() + "\n");
                    }
                }
                sb.append("responseData=" + ret);
                logger.info(logFlag + "{}", sb);
            }

        } finally {
            releaseRequest(httpPost, response);
        }
        return ret;
    }

    private void releaseRequest(HttpRequestBase request, CloseableHttpResponse response) {
        try {
            if (response != null && response.getEntity() != null) {
                EntityUtils.consume(response.getEntity());
            }
        } catch (Exception e) {
            logger.error("releaseRequest", e);
        }
        close(response);
    }

    public void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                logger.error("close", e);
            }
        }
    }
}
